/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Alexander Y. Kleymenov
 */

package org.apache.harmony.security.provider.cert;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CRLException;
import java.security.cert.Certificate;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import org.apache.harmony.security.internal.nls.Messages;
import org.apache.harmony.security.utils.AlgNameMapper;
import org.apache.harmony.security.x509.CertificateList;
import org.apache.harmony.security.x509.Extension;
import org.apache.harmony.security.x509.Extensions;
import org.apache.harmony.security.x509.TBSCertList;

/**
 * This class is an implementation of X509CRL. It wraps the instance of
 * org.apache.harmony.security.x509.CertificateList built on the base of
 * provided ASN.1 DER encoded form of CertificateList structure (as specified in
 * RFC 3280 http://www.ietf.org/rfc/rfc3280.txt). Implementation supports work
 * with indirect CRLs.
 * 
 * @see org.apache.harmony.security.x509.CertificateList
 * @see java.security.cert.X509CRL
 */
public class X509CRLImpl extends X509CRL {

	// the core object to be wrapped in X509CRL
	private final CertificateList crl;

	// To speed up access to the info, the following fields
	// cache values retrieved from the CertificateList object
	private final TBSCertList tbsCertList;
	private byte[] tbsCertListEncoding;
	private final Extensions extensions;
	private X500Principal issuer;
	private ArrayList<X509CRLEntryImpl> entries;
	private int entriesSize;
	private byte[] signature;
	private String sigAlgOID;
	private String sigAlgName;
	private byte[] sigAlgParams;

	// encoded form of crl
	private byte[] encoding;

	// indicates whether the signature algorithm parameters are null
	private boolean nullSigAlgParams;
	// indicates whether the crl entries have already been retrieved
	// from CertificateList object (crl)
	private boolean entriesRetrieved;

	// indicates whether this X.509 CRL is direct or indirect
	// (see rfc 3280 http://www.ietf.org/rfc/rfc3280.txt, p 5.)
	private boolean isIndirectCRL;
	// if crl is indirect, this field holds an info about how
	// many of the leading certificates in the list are issued
	// by the same issuer as CRL.
	private int nonIndirectEntriesSize;

	/**
	 * Creates X.509 CRL on the base of ASN.1 DER encoded form of the CRL
	 * (CertificateList structure described in RFC 3280) provided via array of
	 * bytes.
	 * 
	 * @throws IOException
	 *             if decoding errors occur.
	 */
	public X509CRLImpl(byte[] encoding) throws IOException {
		this((CertificateList) CertificateList.ASN1.decode(encoding));
	}

	/**
	 * Creates X.509 CRL by wrapping of the specified CertificateList object.
	 */
	public X509CRLImpl(CertificateList crl) {
		this.crl = crl;
		tbsCertList = crl.getTbsCertList();
		extensions = tbsCertList.getCrlExtensions();
	}

	/**
	 * Creates X.509 CRL on the base of ASN.1 DER encoded form of the CRL
	 * (CertificateList structure described in RFC 3280) provided via input
	 * stream.
	 * 
	 * @throws CRLException
	 *             if decoding errors occur.
	 */
	public X509CRLImpl(InputStream in) throws CRLException {
		try {
			// decode CertificateList structure
			crl = (CertificateList) CertificateList.ASN1.decode(in);
			tbsCertList = crl.getTbsCertList();
			extensions = tbsCertList.getCrlExtensions();
		} catch (final IOException e) {
			throw new CRLException(e);
		}
	}

	// ---------------------------------------------------------------------
	// ----- java.security.cert.X509CRL abstract method implementations ----
	// ---------------------------------------------------------------------

	/**
	 * @see java.security.cert.X509Extension#getCriticalExtensionOIDs() method
	 *      documentation for more info
	 */
	@Override
	public Set<String> getCriticalExtensionOIDs() {
		if (extensions == null) {
			return null;
		}
		return extensions.getCriticalExtensions();
	}

	/**
	 * @see java.security.cert.X509CRL#getEncoded() method documentation for
	 *      more info
	 */
	@Override
	public byte[] getEncoded() throws CRLException {
		if (encoding == null) {
			encoding = crl.getEncoded();
		}
		final byte[] result = new byte[encoding.length];
		System.arraycopy(encoding, 0, result, 0, encoding.length);
		return result;
	}

	/**
	 * @see java.security.cert.X509Extension#getExtensionValue(String) method
	 *      documentation for more info
	 */
	@Override
	public byte[] getExtensionValue(String oid) {
		if (extensions == null) {
			return null;
		}
		final Extension ext = extensions.getExtensionByOID(oid);
		return (ext == null) ? null : ext.getRawExtnValue();
	}

	/**
	 * @see java.security.cert.X509CRL#getIssuerDN() method documentation for
	 *      more info
	 */
	@Override
	public Principal getIssuerDN() {
		if (issuer == null) {
			issuer = tbsCertList.getIssuer().getX500Principal();
		}
		return issuer;
	}

	/**
	 * @see java.security.cert.X509CRL#getIssuerX500Principal() method
	 *      documentation for more info
	 */
	@Override
	public X500Principal getIssuerX500Principal() {
		if (issuer == null) {
			issuer = tbsCertList.getIssuer().getX500Principal();
		}
		return issuer;
	}

	/**
	 * @see java.security.cert.X509CRL#getNextUpdate() method documentation for
	 *      more info
	 */
	@Override
	public Date getNextUpdate() {
		return tbsCertList.getNextUpdate();
	}

	/**
	 * @see java.security.cert.X509Extension#getNonCriticalExtensionOIDs()
	 *      method documentation for more info
	 */
	@Override
	public Set<String> getNonCriticalExtensionOIDs() {
		if (extensions == null) {
			return null;
		}
		return extensions.getNonCriticalExtensions();
	}

	/**
	 * Method searches for CRL entry with specified serial number. The method
	 * will search only certificate issued by CRL's issuer.
	 * 
	 * @see java.security.cert.X509CRL#getRevokedCertificate(BigInteger) method
	 *      documentation for more info
	 */
	@Override
	public X509CRLEntry getRevokedCertificate(BigInteger serialNumber) {
		if (!entriesRetrieved) {
			retrieveEntries();
		}
		if (entries == null) {
			return null;
		}
		for (int i = 0; i < nonIndirectEntriesSize; i++) {
			final X509CRLEntry entry = entries.get(i);
			if (serialNumber.equals(entry.getSerialNumber())) {
				return entry;
			}
		}
		return null;
	}

	/**
	 * Searches for certificate in CRL. This method supports indirect CRLs: if
	 * CRL is indirect method takes into account serial number and issuer of the
	 * certificate, if CRL issued by CA (i.e. it is not indirect) search is done
	 * only by serial number of the specified certificate.
	 * 
	 * @see java.security.cert.X509CRL#getRevokedCertificate(X509Certificate)
	 *      method documentation for more info
	 */
	@Override
	public X509CRLEntry getRevokedCertificate(X509Certificate certificate) {
		if (certificate == null) {
			throw new NullPointerException();
		}
		if (!entriesRetrieved) {
			retrieveEntries();
		}
		if (entries == null) {
			return null;
		}
		final BigInteger serialN = certificate.getSerialNumber();
		if (isIndirectCRL) {
			// search in indirect crl
			X500Principal certIssuer = certificate.getIssuerX500Principal();
			if (certIssuer.equals(getIssuerX500Principal())) {
				// certificate issuer is CRL issuer
				certIssuer = null;
			}
			for (int i = 0; i < entriesSize; i++) {
				final X509CRLEntry entry = entries.get(i);
				// check the serial number of revoked certificate
				if (serialN.equals(entry.getSerialNumber())) {
					// revoked certificate issuer
					final X500Principal iss = entry.getCertificateIssuer();
					// check the issuer of revoked certificate
					if (certIssuer != null) {
						// certificate issuer is not a CRL issuer, so
						// check issuers for equality
						if (certIssuer.equals(iss)) {
							return entry;
						}
					} else if (iss == null) {
						// both certificates was issued by CRL issuer
						return entry;
					}
				}
			}
		} else {
			// search in CA's (non indirect) crl: just look up the serial number
			for (int i = 0; i < entriesSize; i++) {
				final X509CRLEntry entry = entries.get(i);
				if (serialN.equals(entry.getSerialNumber())) {
					return entry;
				}
			}
		}
		return null;
	}

	/**
	 * @see java.security.cert.X509CRL#getRevokedCertificates() method
	 *      documentation for more info
	 */
	@Override
	public Set<? extends X509CRLEntry> getRevokedCertificates() {
		if (!entriesRetrieved) {
			retrieveEntries();
		}
		if (entries == null) {
			return null;
		}
		return new HashSet<X509CRLEntryImpl>(entries);
	}

	/**
	 * @see java.security.cert.X509CRL#getSigAlgName() method documentation for
	 *      more info
	 */
	@Override
	public String getSigAlgName() {
		if (sigAlgOID == null) {
			sigAlgOID = tbsCertList.getSignature().getAlgorithm();
			sigAlgName = AlgNameMapper.map2AlgName(sigAlgOID);
			if (sigAlgName == null) {
				sigAlgName = sigAlgOID;
			}
		}
		return sigAlgName;
	}

	/**
	 * @see java.security.cert.X509CRL#getSigAlgOID() method documentation for
	 *      more info
	 */
	@Override
	public String getSigAlgOID() {
		if (sigAlgOID == null) {
			sigAlgOID = tbsCertList.getSignature().getAlgorithm();
			sigAlgName = AlgNameMapper.map2AlgName(sigAlgOID);
			if (sigAlgName == null) {
				sigAlgName = sigAlgOID;
			}
		}
		return sigAlgOID;
	}

	/**
	 * @see java.security.cert.X509CRL#getSigAlgParams() method documentation
	 *      for more info
	 */
	@Override
	public byte[] getSigAlgParams() {
		if (nullSigAlgParams) {
			return null;
		}
		if (sigAlgParams == null) {
			sigAlgParams = tbsCertList.getSignature().getParameters();
			if (sigAlgParams == null) {
				nullSigAlgParams = true;
				return null;
			}
		}
		return sigAlgParams;
	}

	/**
	 * @see java.security.cert.X509CRL#getSignature() method documentation for
	 *      more info
	 */
	@Override
	public byte[] getSignature() {
		if (signature == null) {
			signature = crl.getSignatureValue();
		}
		final byte[] result = new byte[signature.length];
		System.arraycopy(signature, 0, result, 0, signature.length);
		return result;
	}

	/**
	 * @see java.security.cert.X509CRL#getTBSCertList() method documentation for
	 *      more info
	 */
	@Override
	public byte[] getTBSCertList() throws CRLException {
		if (tbsCertListEncoding == null) {
			tbsCertListEncoding = tbsCertList.getEncoded();
		}
		final byte[] result = new byte[tbsCertListEncoding.length];
		System.arraycopy(tbsCertListEncoding, 0, result, 0,
				tbsCertListEncoding.length);
		return result;
	}

	/**
	 * @see java.security.cert.X509CRL#getThisUpdate() method documentation for
	 *      more info
	 */
	@Override
	public Date getThisUpdate() {
		return tbsCertList.getThisUpdate();
	}

	/**
	 * @see java.security.cert.X509CRL#getVersion() method documentation for
	 *      more info
	 */
	@Override
	public int getVersion() {
		return tbsCertList.getVersion();
	}

	// ---------------------------------------------------------------------
	// ------ java.security.cert.CRL abstract method implementations -------
	// ---------------------------------------------------------------------

	/**
	 * @see java.security.cert.X509Extension#hasUnsupportedCriticalExtension()
	 *      method documentation for more info
	 */
	@Override
	public boolean hasUnsupportedCriticalExtension() {
		if (extensions == null) {
			return false;
		}
		return extensions.hasUnsupportedCritical();
	}

	/**
	 * @see java.security.cert.CRL#isRevoked(Certificate) method documentation
	 *      for more info
	 */
	@Override
	public boolean isRevoked(Certificate cert) {
		if (!(cert instanceof X509Certificate)) {
			return false;
		}
		return getRevokedCertificate((X509Certificate) cert) != null;
	}

	// ---------------------------------------------------------------------
	// ------ java.security.cert.X509Extension method implementations ------
	// ---------------------------------------------------------------------

	/*
	 * Retrieves the crl entries (TBSCertList.RevokedCertificate objects) from
	 * the TBSCertList structure and converts them to the X509CRLEntryImpl
	 * objects
	 */
	private void retrieveEntries() {
		entriesRetrieved = true;
		final List rcerts = tbsCertList.getRevokedCertificates();
		if (rcerts == null) {
			return;
		}
		entriesSize = rcerts.size();
		entries = new ArrayList<X509CRLEntryImpl>(entriesSize);
		// null means that revoked certificate issuer is the same as CRL issuer
		X500Principal rcertIssuer = null;
		for (int i = 0; i < entriesSize; i++) {
			final TBSCertList.RevokedCertificate rcert = (TBSCertList.RevokedCertificate) rcerts
					.get(i);
			final X500Principal iss = rcert.getIssuer();
			if (iss != null) {
				// certificate issuer differs from CRL issuer
				// and CRL is indirect.
				rcertIssuer = iss;
				isIndirectCRL = true;
				// remember how many leading revoked certificates in the
				// list are issued by the same issuer as issuer of CRL
				// (these certificates are first in the list)
				nonIndirectEntriesSize = i;
			}
			entries.add(new X509CRLEntryImpl(rcert, rcertIssuer));
		}
	}

	/**
	 * @see java.security.cert.CRL#toString() method documentation for more info
	 */
	@Override
	public String toString() {
		return crl.toString();
	}

	/**
	 * @see java.security.cert.X509CRL#verify(PublicKey key) method
	 *      documentation for more info
	 */
	@Override
	public void verify(PublicKey key) throws CRLException,
			NoSuchAlgorithmException, InvalidKeyException,
			NoSuchProviderException, SignatureException {
		final Signature signature = Signature.getInstance(getSigAlgName());
		signature.initVerify(key);
		final byte[] tbsEncoding = tbsCertList.getEncoded();
		signature.update(tbsEncoding, 0, tbsEncoding.length);
		if (!signature.verify(crl.getSignatureValue())) {
			throw new SignatureException(Messages.getString("security.15C")); //$NON-NLS-1$
		}
	}

	/**
	 * @see java.security.cert.X509CRL#verify(PublicKey key, String sigProvider)
	 *      method documentation for more info
	 */
	@Override
	public void verify(PublicKey key, String sigProvider) throws CRLException,
			NoSuchAlgorithmException, InvalidKeyException,
			NoSuchProviderException, SignatureException {
		final Signature signature = Signature.getInstance(getSigAlgName(),
				sigProvider);
		signature.initVerify(key);
		final byte[] tbsEncoding = tbsCertList.getEncoded();
		signature.update(tbsEncoding, 0, tbsEncoding.length);
		if (!signature.verify(crl.getSignatureValue())) {
			throw new SignatureException(Messages.getString("security.15C")); //$NON-NLS-1$
		}
	}
}
